/*
 * Copyright (c) 2009, Jeremy Sandell
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the author nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <memory>

#include "common.h"
#include "net/socket.h"
#include "except.h"

namespace hl7 {


    Socket::Socket() : sockfd ( -1 )
    {
        /* Zero out our sockaddr_in member. */
        memset(&m_addr, 0, sizeof(m_addr));
    }

    Socket::Socket(const std::string & hostname) : sockfd ( -1 )
    {
        /* Zero out our sockaddr_in member. */
        memset(&m_addr, 0, sizeof(m_addr));

        try
        {
            if(!create())
                throw Exception("Couldn't create socket.");

            m_addr.sin_family = AF_INET;

            if(!resolve(hostname))
                throw Exception("Couldn't resolve " + hostname);

            host = hostname;
        }
        catch(...)
        {
            delete h;
            throw;
        }
    }

    Socket::~Socket()
    {
        if (valid())
            ::close(sockfd);
    }

    /** 
     * \fn create
     * \brief Create the internal socket.
     * \returns True if call to socket() is successful, false if
     * it (or setting our socket options) fails.
     */

    bool Socket::create()
    {
        int tw; /* TIME_WAIT */ 
        sockfd = ::socket(AF_INET, SOCK_STREAM, 0);

        if (!valid())
            return false;

        tw = 1;

        /* TODO: As noted in bind and listen, need to peek 
         * at return value and act accordingly.
         */

        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,(const char *)&tw, sizeof(tw)) == -1)
            return false;

        return true;
    }

    /** 
     * \fn bind
     * \brief Bind Socket to port.
     * \param const int port
     * \returns True if call to bind() is successful, false if socket 
     * is invalid or call fails.
     */

    bool Socket::bind (const int port)
    {
        if (!valid())
            return false;

        m_addr.sin_addr.s_addr = INADDR_ANY;
        m_addr.sin_port = htons (port);

        int bind_ret = ::bind(sockfd,(struct sockaddr *)&m_addr, sizeof (m_addr));

        /* TODO: As with the listen() call below, this can return a 
         * multitude of informative values. While the basic implementation
         * is OK to use a boolean value, I think it might be better 
         * (that is, make it easier to debug) if we inspect the return 
         * value and act accordingly.
         */

        if (bind_ret == -1)
            return false;

        return true;
    }

    /** 
     * \fn resolve
     * \brief resolve hostname, update object (only if we can resolve it).
     * \param const std::string &host
     * \returns True if able to resolve `host`, false otherwise.
     */

    bool Socket::resolve(const std::string &host)
    {
        h = gethostbyname(host.c_str());

        if(h != NULL)
        {
            /* FIXME: Make this be able to use newer function 
             * lookup. (i.e., getnameinfo/getaddrinfo).
             */
            memcpy((char *)&m_addr.sin_addr.s_addr,(char *)h->h_addr, h->h_length);
            return true;
        }
        return false;
    }

#ifdef USE_OLD_RESOLVE
    bool Socket::lookup(const std::string &s, std::string &resolved)
    {
        struct hostent *hp;

        if(inet_aton(s.c_str(), NULL))
        {
            /* Is an IP address */
            hp = gethostbyaddr((char*)&addr, sizeof(addr), AF_INET);

            if(hp != NULL)
            {
                resolved = hp->h_name;
                return true;
            }
        }
        else
        {
            /* Not an IP address */
            hp = gethostbyname(hostname.c_str());

            if(hp!=NULL)
            {
                resolved = inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]);
                return true;
            }
        }
        return false;
    }
#else
    /* TODO: maybe overload this so that, in the case of 
     * multiple responses, we can provide a vector? Something
     * to think on, anyway.
     */
    bool Socket::lookup(const std::string &s, std::string &resolved)
    {
        char hostbuf[NI_MAXHOST];
        struct addrinfo *res;
        struct addrinfo hints;
        const char *addr;
        int     err;

        memset((char *) &hints, 0, sizeof(hints));

        hints.ai_family = AF_INET; /* AF_INET - ipv4.
                                      * AF_INET6 - ipv6.
                                      * PF_UNSPEC - both / either.
                                      */

        hints.ai_flags = AI_CANONNAME; /* Flag to populate ai_canonname 
                                        * with the 'official' name for the host.
                                        */

        hints.ai_socktype = SOCK_STREAM; /* NOTE: use 0 for any socket type. */

        if ((err = getaddrinfo(s.c_str(), NULL, &hints, &res)) != 0)
        {
            /*host/address lookup failure.*/
            freeaddrinfo(res);
            return false;
        }

        if(inet_aton(s.c_str(), NULL))
        {
            /* We've been given an IP, retreive the name. */
            err = getnameinfo(res->ai_addr, res->ai_addrlen, hostbuf, 
                              sizeof(hostbuf), NULL, 0, NI_NAMEREQD);

            if(err)
            {
                /* getnameinfo failed. Free our resources and bail.
                 * Note - could use gai_strerror(err) to inspect
                 * the error.
                 */
                freeaddrinfo(res);
                return false;
            }
        }
        else
        {
            /* We've been given a host, get the IP address. */
            addr = (char *) &((struct sockaddr_in *) res->ai_addr)->sin_addr;

            if (inet_ntop(res->ai_family, addr, hostbuf, sizeof(hostbuf)) == 0)
            {
                freeaddrinfo(res);
                return false;
            }
        }
        resolved = hostbuf;
        freeaddrinfo(res);
        return true;
    }
#endif


    /** 
     * \fn listen
     * \brief listen for accept new socket connection.
     * \param int max - maximum number of connections allowed in backlog.
     * \returns True if call to listen()is successful, or false otherwise.
     */

    bool Socket::listen(int max) const
    {
        if (!valid())
            return false;

        int listen_ret = ::listen(sockfd, max);

        /*
         * TODO: Update to inspect the return values, as according to the man 
         * page there are several possibilities. Noting for future implementation:
         *
         * The listen() function shall fail if:
         *
         * EBADF  The socket argument is not a valid file descriptor.
         * EDESTADDRREQ The socket is not bound to a local address, 
         * and the protocol does not support listening  on  an unbound socket.
         *
         * EINVAL The socket is already connected.
         * ENOTSOCK The socket argument does not refer to a socket.
         * EOPNOTSUPP The socket protocol does not support listen().
         *
         * The listen() function may fail if:
         *
         * EACCES The calling process does not have the appropriate privileges.
         * EINVAL The socket has been shut down.
         * ENOBUFS Insufficient resources are available in the system to complete the call.
         */

        if (listen_ret == -1)
            return false;

        return true;
    } 

    /** 
     * \fn accept
     * \brief accept new socket connection.
     * \param Socket &newSocket
     * \returns True if connection is successful, or false otherwise.
     */

    bool Socket::accept (Socket &newSocket) const
    {
        int addr_length = sizeof(m_addr);
        newSocket.sockfd = ::accept(sockfd, (sockaddr *)&m_addr, (socklen_t *)&addr_length);

        /* TODO: This might be more useful if it returned the 
         * descriptor for the connection instead of just a boolean
         * value.
         */

        if (newSocket.sockfd <= 0)
            return false;
        else
            return true;
    }

    /** 
     * \fn send
     * \brief Send data over a socket.
     * \param const std::string s - Data to send.
     * \returns True if send is successful, given a positive argument, or 
     *  false otherwise.
     */

    bool Socket::send(const std::string s) const
    {
        int status = ::send(sockfd, s.c_str(), s.size(), MSG_NOSIGNAL); /* Don't generate SIGPIPE. */

        if (status == -1)
            return false;
        else
            return true;
    }

    /** 
     * \fn recv
     * \brief Get data over a socket.
     *
     * \param std::string &s - buffer to get data into.
     * \param int max - maximum to receive. Optional, if not supplied will get as much as it can.
     * \param int chunks - size of buffer to receive. Optional, defaults to 64 bytes.
     *
     * \returns True if send is successful, given a positive argument, or 
     *  false otherwise.
     */

    int Socket::recv(std::string &s, int max, int chunks) const
    {
        char *buf = NULL;
        int status = 0;
        s.clear();

        if (max > 0)
        {
            if((buf = (char*)calloc(1,sizeof(char) * (max + 1))) == NULL)
                throw Exception("Out of memory.");

            memset ( buf, 0, max + 1 );
            status = ::recv ( sockfd, buf, max, 0 );

            if ( status == -1 )
            {
                free(buf);
                throw Exception("Receive error: " + std::string(strerror(errno)));
            }
            else if ( status == 0 )
                free(buf);
            else
            {
                s = buf;
                free(buf);
            }
        }
        else
        {
            if((buf = (char*)calloc(1,sizeof(char) * chunks)) == NULL)
                throw Exception("Out of memory.");

            while((status = ::recv(sockfd, buf, chunks, 0)) > 0)
                s.append(buf,status);

            status=s.length();
            free(buf);
        }
        return status;
    }

    /** 
     * \fn recv_wait
     * \brief Set socket receive timeout.
     * \param int ms - How long to wait for data, in milliseconds.
     *
     * \returns False if not given a positive argument, or setsockopt fails; true otherwise.
     */

    bool Socket::recv_wait(int ms)
    {
        if (ms > 0)
        {
            struct timeval tv;

            tv.tv_sec = ms / 1000;
            tv.tv_usec = ( ms % 1000) * 1000;

            if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof tv) == -1)
                return false;

            return true;
        }
        return false;
    }

    /** 
     * \fn send_wait
     * \brief Set socket send timeout.
     * \param int ms - How long to wait, in milliseconds.
     *
     * \returns False if not given a positive argument, or setsockopt fails; true otherwise.
     */

    bool Socket::send_wait(int ms)
    {
        if (ms > 0)
        {
            struct timeval tv;

            tv.tv_sec = ms / 1000;
            tv.tv_usec = (ms % 1000) * 1000;

            if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof tv) == -1)
                return false;

            return true;
        }
        return false;
    }

    /** 
     * \fn connect
     * \brief Connect to specified host.
     * \param const std::string host - host to connect to.
     * \param const int port - port to connect to.
     *
     * \returns True if successful, or false if connect fails, or socket is invalid.
     */

    bool Socket::connect(const std::string host, const int port)
    {
        if (!valid())
            return false;

        std::string res;
        int status;

        if(lookup(host, res))
        {
            m_addr.sin_family = AF_INET;
            m_addr.sin_port = htons(port);

            status = inet_pton(AF_INET, res.c_str(), &m_addr.sin_addr);

            if (errno == EAFNOSUPPORT)
                return false;

            status = ::connect(sockfd, (sockaddr *) &m_addr, sizeof(m_addr));
        }
        else
            status = 1; // couldn't lookup host.

        if (status == 0)
            return true;
        else
            return false;
    }

    /** 
     * \fn set_non_blocking
     * \brief Set socket to either block, or not block.
     * \param const bool block - True to turn off blocking, false to turn on blocking.
     *
     * \returns void.
     */

    void Socket::set_non_blocking(const bool block)
    {
        int opts;

        opts = fcntl(sockfd, F_GETFL);

        /* TODO: Have this throw an exception? Silently failing
         * is rarely nice, and often makes things a pain to debug.
         */

        if (opts < 0)
            return;

        if (block)
            opts = (opts | O_NONBLOCK);
        else
            opts = (opts & ~O_NONBLOCK);

        fcntl(sockfd, F_SETFL, opts);
    }
};
